package https

import (
	"crypto/tls"
	"errors"
	"fmt"
	"github.com/lunny/log"
	"io"
	"io/ioutil"
	"math/rand"
	"net"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const (
	DEFAULT_PORT       = 80  // 默认请求端口: 80, 当没有指定端口时, 使用默认端口
	HTTPS_DEFAULT_PORT = 443 // 默认 https 请求端口
	DEFAULT_TIMEOUT    = 120 // 超时时间, 默认 2分钟 单位: 秒
)

// 每次请求之间的间隔
var (
	// 连续两次请求之间的最短时间间隔
	REQUEST_FREQUENRY_MILLISEC time.Duration = 200

	// 请求失败后的最大重试次数
	REQUEST_RETRY_COUNT = 1

	// 请求失败后的网络情况判别参考网址, 如 https://www.baidu.com
	REQUET_FAIL_REFERENCE_URLS = []string{
		"https://www.baidu.com",
	}

	// 全局缓存管理器
	Jar, _ = cookiejar.New(nil)
)

// https 请求任务接口
type Task interface {
	// 执行请求
	Execute() (content []byte, code int, timeMilis int64, serverName string, err error) // 执行方法
	// 执行简式请求
	ExecuteWithResponse() (resp *http.Response, err error)
	// 执行方法
	scan(checkProxyIpFunc func() bool) (content []byte, code int, timeMilis int64, serverName string, err error) // 执行方法
	// 执行方法
	pureScan(checkProxyIpFunc func() bool) (timeMilis int64, resp *http.Response, err error) // 执行方法
	// 对 pureScan 的失败重试封装
	pureRetryScan(checkProxyIpFunc func() bool) (timeMilis int64, resp *http.Response, err error) // 执行方法
}

// 默认实现, 代理使用舒徐： 代理地址 > proxytoken获取地址 > proxyToken 为空不使用代理
type HttpScanTask struct {
	host       string            // 主机地址
	service    ServicePort       // 请求端口
	timeout    int               // 请求超时时间
	path       string            // 请求路径
	proxy      string            // 代理地址
	headers    map[string]string // 请求头
	cookies    []*http.Cookie    // cookies 管理器, 出站发送(非空), 入站更新
	method     string            // 请求方法
	body       string            // 请求体
	retryCount int               // 请求失败后的重试次数, 0, 取默认值, -1, 不重试, >0 重试 retryCount 次
}

// 重试次数配置
func (task *HttpScanTask) retryCountNum() int {
	count := REQUEST_RETRY_COUNT
	if task.retryCount < 0 {
		count = 1
	} else if task.retryCount > 0 {
		count = task.retryCount
	}
	return count
}

// 请求端口默认实现
type ServicePort struct {
	ssl  bool
	port int
}

// 请求端口的工厂方法
func NewServicePort(ssl bool, port int) ServicePort {
	return ServicePort{ssl: ssl, port: port}
}

// ================================== 无 代理方法 ===================================

//=========================schemea+host+port+path 无 代理请求=====================
// 普通 请求方法
func PostScan(host, path string, timeout int, servicePort ServicePort, retryCount int) *HttpScanTask {
	return NewScanProxy("POST", host, path, "", timeout, servicePort, retryCount)
}

// 普通 请求方法
func GetScan(host, path string, timeout int, servicePort ServicePort, retryCount int) *HttpScanTask {
	return NewScanProxy("GET", host, path, "", timeout, servicePort, retryCount)
}

// 普通 请求方法
func NormalScan(method, host, path string, timeout int, servicePort ServicePort, retryCount int) *HttpScanTask {
	return NewScanProxy(method, host, path, "", timeout, servicePort, retryCount)
}

//=========================url 无 代理请求=====================
// POST 无代理
func POSTUrlScan(url string, timeout int, retryCount int) (*HttpScanTask, error) {
	return NewUrlScanProxy("POST", url, "", timeout, retryCount)
}

// Get 无代理
func GetUrlScan(url string, timeout int, retryCount int) (*HttpScanTask, error) {
	return NewUrlScanProxy("GET", url, "", timeout, retryCount)
}

// 默认 不带 代理
func NormalUrlScan(method, url string, timeout int, retryCount int) (*HttpScanTask, error) {
	return NewUrlScanProxy(method, url, "", timeout, retryCount)
}

// ================================== 代理方法 ===================================

//=========================schemea+host+port+path 方式 代理请求=====================

// 普通 请求方法
func PostScanProxy(host, path, proxy string, timeout int, servicePort ServicePort, retryCount int) *HttpScanTask {
	return NewScan("POST", host, path, proxy, timeout, servicePort, nil, nil, "", retryCount)
}

// 普通 请求方法
func GetScanProxy(host, path, proxy string, timeout int, servicePort ServicePort, retryCount int) *HttpScanTask {
	return NewScan("GET", host, path, proxy, timeout, servicePort, nil, nil, "", retryCount)
}

// 普通 请求方法
func NewScanProxy(method, host, path, proxy string, timeout int, servicePort ServicePort, retryCount int) *HttpScanTask {
	return NewScan(method, host, path, proxy, timeout, servicePort, nil, nil, "", retryCount)
}

//=========================url 方式 代理请求=====================

// url 普通 请求方法
func PostUrlScanProxy(url, proxy string, timeout int, retryCount int) (*HttpScanTask, error) {
	return NewUrlScan("POST", url, proxy, timeout, nil, nil, "", retryCount)
}

// url 普通 请求方法
func GetUrlScanProxy(url, proxy string, timeout int, retryCount int) (*HttpScanTask, error) {
	return NewUrlScan("GET", url, proxy, timeout, nil, nil, "", retryCount)
}

// url 普通 请求方法
func NewUrlScanProxy(method, url, proxy string, timeout int, retryCount int) (*HttpScanTask, error) {
	return NewUrlScan(method, url, proxy, timeout, nil, nil, "", retryCount)
}

// ================================================== 所有参数方法==============================================================

// url + 所有参数
func NewUrlScan(method, urlAddr, proxy string, timeout int, cookies []*http.Cookie, headers map[string]string, body string, retryCount int) (*HttpScanTask, error) {
	host, path, servicePort, err := resolveUrl(urlAddr)
	if err != nil {
		return nil, err
	}

	return &HttpScanTask{method: method, host: host, service: servicePort, path: path, proxy: proxy, timeout: timeout, cookies: cookies, headers: headers, body: body, retryCount: retryCount}, nil
}

// 所有参数
func NewScan(method, host, path string, proxy string, timeout int, servicePort ServicePort, cookies []*http.Cookie, headers map[string]string, body string, retryCount int) *HttpScanTask {
	return &HttpScanTask{method: method, host: host, service: servicePort, path: path, proxy: proxy, timeout: timeout, cookies: cookies, headers: headers, body: body, retryCount: retryCount}
}

// pureScan 简单响应， 仅有 响应体和请求时间
// return   timeMilis 请求时间； resp 响应体； err 执行错误
func (task *HttpScanTask) pureScan(checkProxyIpFunc func() bool) (timeMilis int64, resp *http.Response, err error) {
	var schema string
	// 根据协议和传参， 指定请求端口号
	if task.service.ssl {
		schema = "https://"
		if task.service.port <= 0 {
			task.service.port = HTTPS_DEFAULT_PORT
		}
	} else {
		schema = "http://"
		if task.service.port <= 0 {
			task.service.port = DEFAULT_PORT
		}
	}

	// 拼接请求地址
	link := ""
	if !strings.HasPrefix(task.path, "/") {
		task.path += "/"
	}
	if task.service.port != DEFAULT_PORT && task.service.port != HTTPS_DEFAULT_PORT {
		link = schema + task.host + ":" + strconv.Itoa(task.service.port) + task.path
	} else {
		link = schema + task.host + task.path
	}

	log.Printf("== pureScan ==>> request url is : %v \n", link)

	// 配置连接参数
	transport := &http.Transport{
		// tcp 链接配置
		DialContext: (&net.Dialer{
			Timeout:   60 * time.Second, // 等待 tcp 连接建立的最长时间, 默认为 1min
			KeepAlive: 15 * time.Second, // 监测连接可用性的频率, 当为 0 时, 采用默认值 15s
		}).DialContext,
		// 跳过 https 证书验证
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
		// 超时设置
		TLSHandshakeTimeout:   10 * time.Second, // 等待 tls 握手成功的超时时间
		ResponseHeaderTimeout: 10 * time.Second, // 发出请求后, 接收服务器响应的超时时间
		ExpectContinueTimeout: 1 * time.Second,
		IdleConnTimeout:       1 * time.Nanosecond,
		MaxIdleConnsPerHost:   1,
		MaxIdleConns:          1,
	}

	// 超时时间
	timeout := DEFAULT_TIMEOUT * time.Second
	if task.timeout > 0 {
		timeout = time.Second * time.Duration(task.timeout)
	}

	client := &http.Client{
		Transport: transport,
		Timeout:   timeout, // 请求时间
		Jar:       Jar,
	}

	// 若没有配置默认请求方法， 使用 GET 请求方式
	method := "GET"
	if task.method != "" {
		method = task.method
	}
	req, err := http.NewRequest(method, link, nil)
	if err != nil {
		return
	}

	// 添加cookie 数据
	if task.cookies != nil {
		client.Jar.SetCookies(req.URL, task.cookies)
	}

	// 配置默认请求头
	req.Header.Set("User-Agent", getAgent())
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("sec-ch-ua", "\"Google Chrome\";v=\"87\", \"\\\"Not;A\\\\Brand\";v=\"99\", \"Chromium\";v=\"87\"")
	req.Header.Set("host", task.host)
	// 更新或添加附加请求头
	for i, v := range task.headers {
		req.Header.Del(i)
		req.Header.Set(i, v)
	}

	// 设置 http 代理, 格式如: http://127.0.0.1:8080
	if task.proxy != "" {
		proxy := func(_ *http.Request) (*url.URL, error) {
			return url.Parse(task.proxy)
		}
		transport.Proxy = proxy
	}

	// 设置请求体
	if task.body != "" {
		var body io.Reader = strings.NewReader(task.body)
		rc, ok := body.(io.ReadCloser)
		if !ok && body != nil {
			rc = ioutil.NopCloser(body)
		}
		req.Body = rc
	}

	// 发起请求, 并统计请求耗时
	now := time.Now()
	resp, err = client.Do(req)
	end := time.Now()
	timeMilis = end.Sub(now).Milliseconds()

	// 设置两个连续请求之间的 最短时间间隔
	time.Sleep(time.Millisecond * REQUEST_FREQUENRY_MILLISEC)
	return
}

func (task *HttpScanTask) pureRetryScan(checkProxyIpFunc func() bool) (timeMilis int64, resp *http.Response, err error) {
	for i := 0; i < task.retryCountNum(); i++ {
		timeMilis, resp, err = task.pureScan(checkProxyIpFunc)
		if err != nil || (resp != nil && resp.StatusCode != 200) {
			// 没收到响应体, 默认响应码为 -1
			respCode := -1
			if resp != nil {
				respCode = resp.StatusCode
			}
			log.Errorf("==pureRetryScan==> 请求[%v]失败, 响应码为: %v, 失败信息为:%v", task.host, respCode, err)
		} else {
			break
		}
	}

	return
}

// scan 扫描方法
// 返回参数:  响应体, 响应码, 请求时间, 服务器类型, 错误
func (task *HttpScanTask) scan(checkProxyIpFunc func() bool) (content []byte, code int, timeMilis int64, serverName string, err error) {
	// 发送请求
	var resp *http.Response
	timeMilis, resp, err = task.pureRetryScan(checkProxyIpFunc)
	if err != nil {
		return
	}
	// 读取响应头
	if resp != nil {
		code = resp.StatusCode
		serverName = resp.Header.Get("Server")
	}
	// 读取响应体
	content, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Printf("===>> %v \n", err)
		return
	}
	err = resp.Body.Close()
	if err != nil {
		log.Printf("===>> %v \n", err)
		return
	}
	// 请求间隔控制
	return
}

// 响应体, 响应码, 请求时间, 服务器名称, 错误信息
func (task *HttpScanTask) Execute() (content []byte, code int, timeMilis int64, serverName string, err error) {
	return task.scan(nil) // 执行任务
}

func (task *HttpScanTask) ExecuteWithResponse() (resp *http.Response, err error) {
	_, resp, err = task.pureRetryScan(nil)
	return
}

// getAgent
func getAgent() string {
	agent := [...]string{
		"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0",
		"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
		"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
		"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
		"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
		"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
		"User-Agent,Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
		"User-Agent, Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
		"User-Agent,Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
		"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36",
	}
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	len := len(agent)
	return agent[r.Intn(len)]
}

// 通过请求地址, 获取主机， 端口 请求路径, 此方法可以通过 url.Parse() 替代
func resolveUrl(urlAddr string) (host, path string, port ServicePort, err error) {
	compile := regexp.MustCompile(`(?i)(https|http)://([^:/]+)(:)?([0-9]*)(.*)`)
	submatch := compile.FindAllStringSubmatch(urlAddr, -1)
	if submatch != nil {
		host = submatch[0][2]
		path = submatch[0][5]
		portNum := 0
		if submatch[0][4] != "" {
			portNum, _ = strconv.Atoi(submatch[0][4])
		}
		port = NewServicePort(strings.ToLower(submatch[0][1]) == "https", portNum)
	} else {
		err = errors.New(fmt.Sprintf("地址格式错误, 请核对后重试, 地址为： %v", urlAddr))
	}

	//parse, err1 := url.Parse(urlAddr)
	//if err != nil {
	//	err = err1
	//	return
	//}
	//
	//host = parse.Host
	//path = parse.Path
	//
	//portNum, err1 := strconv.Atoi(parse.Port())
	//if err != nil {
	//	err = err1
	//	return
	//}
	//port = NewServicePort(strings.ToLower(parse.Scheme) == "https", portNum)

	return
}
